/**
 * 
 */
package jmine.tec.utils.cripto.key;

import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;

import jmine.tec.utils.cripto.exception.CriptoException;
import jmine.tec.utils.io.IOUtils;

/**
 * Esta classe provê um método estático que gera o par de chaves com o algorítmo RSA.
 * <p>
 * O {@link KeyPairGenerator} utilizado é guardado em uma referncia fraca para diminuir o custo de criar e, ao mesmo tempo, deixar o gc
 * consumir o {@link KeyPairGenerator}.
 * 
 * @author gigante
 */
public final class GeradorChaveRSA {

    private static final int NUM16 = 16;

    private static volatile SoftReference<KeyPairGenerator> weakRef;

    /**
     * Este método gera o par de chaves RSA para o tamanho de chave passado.
     * 
     * @param tamanhoChave o tamanho da chave a ser gerada. Deve ser menor que 2^16.
     * @return o {@link KeyPair} com a chave pública e a chave privada em formato RSA.
     */
    public static KeyPair geraParChavesRSA(final int tamanhoChave) {
        return geraParChavesRSAComSeed(tamanhoChave, System.currentTimeMillis());
    }

    /**
     * Gera o par de chaves RSA para o tamanho passao, usando como seed para o gerador de numeros aleatorios o numero dado.
     * 
     * @param tamanho int
     * @param seed long
     * @return {@link KeyPair}
     */
    public static KeyPair geraParChavesRSAComSeed(final int tamanho, final long seed) {
        if (tamanho > 1 << NUM16) {
            throw new CriptoException("Tamanho da chave inválido. Deve ser menor que 2^16.");
        }
        KeyPairGenerator generator = getKeyGenerator();
        SecureRandom random = null;
        try {
            random = SecureRandom.getInstance("SHA1PRNG");
        } catch (NoSuchAlgorithmException e) {
            throw new CriptoException("Cannot create a SecureRandom for SHA1PRNG");
        }
        random.setSeed(longToByteArray(seed));
        generator.initialize(tamanho, random);
        return generator.generateKeyPair();
    }

    /**
     * Pega o {@link KeyPairGenerator} do {@link #weakRef} ou cria se necessário.
     * 
     * @return {@link KeyPairGenerator}
     */
    private static KeyPairGenerator getKeyGenerator() {
        if (weakRef == null) {
            return refreshInstance();
        }
        final KeyPairGenerator keyPairGenerator = weakRef.get();
        if (keyPairGenerator == null) {
            return refreshInstance();
        }
        return keyPairGenerator;
    }

    /**
     * @param theLongValue the long
     * @return the byte array
     */
    private static byte[] longToByteArray(final long theLongValue) {
        final int shiftSize = 8;
        long val = theLongValue;
        byte[] retVal = new byte[shiftSize];

        for (int i = 0; i < shiftSize; i++) {
            retVal[i] = (byte) val;
            val >>= shiftSize;
        }

        return retVal;
    }

    /**
     * Le e constroi uma chave publica de um stream
     * 
     * @param inputStream the input stream
     * @return {@link PublicKey}
     * @throws InvalidKeySpecException se a chave era invalida
     * @throws IOException se o stream estava corrompido
     */
    public static PublicKey leChavePublica(final InputStream inputStream) throws InvalidKeySpecException, IOException {
        return leChavePublica(IOUtils.readFully(inputStream));
    }

    /**
     * Le e constroi uma chave publica de um array de bytes
     * 
     * @param array o array de bytes
     * @return {@link PublicKey}
     * @throws InvalidKeySpecException se a chave era invalida
     */
    public static PublicKey leChavePublica(byte[] array) throws InvalidKeySpecException {
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(array);
        try {
            KeyFactory factory = KeyFactory.getInstance("RSA");
            return factory.generatePublic(keySpec);
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("RSA not available!");
        }
    }

    /**
     * Le e constroi uma chave privada de um stream
     * 
     * @param inputStream o stream
     * @return {@link PrivateKey}
     * @throws InvalidKeySpecException se a chave era invalida
     * @throws IOException se o stream estava corrompido
     */
    public static PrivateKey leChavePrivada(final InputStream inputStream) throws IOException, InvalidKeySpecException {
        return leChavePrivada(IOUtils.readFully(inputStream));
    }

    /**
     * Le e constroi uma chave privada de um array de bytes
     * 
     * @param array de bytes
     * @return {@link PrivateKey}
     * @throws InvalidKeySpecException se a chave era invalida
     */
    public static PrivateKey leChavePrivada(byte[] array) throws InvalidKeySpecException {
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(array);
        try {
            KeyFactory factory = KeyFactory.getInstance("RSA");
            return factory.generatePrivate(keySpec);
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("RSA not available!");
        }
    }

    /**
     * Cria uma nova instancia de {@link KeyPairGenerator}, atualizando o {@link #weakRef}.
     * 
     * @return {@link KeyPairGenerator}
     */
    private static KeyPairGenerator refreshInstance() {
        try {
            final KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
            weakRef = new SoftReference<KeyPairGenerator>(gen);
            return gen;
        } catch (NoSuchAlgorithmException e) {
            throw new CriptoException("No such algorithm: " + e.getMessage());
        }
    }

    /**
     * Copia um cipher para ser usado
     * 
     * @param original {@link Cipher}
     * @return {@link Cipher}
     */
    public static Cipher copyCipher(Cipher original) {
        try {
            return Cipher.getInstance(original.getAlgorithm(), original.getProvider());
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalArgumentException("Should be able to copy an existing cipher", e);
        } catch (NoSuchPaddingException e) {
            throw new IllegalArgumentException("Should be able to copy an existing cipher", e);
        }
    }

    /**
     * C'tor
     */
    private GeradorChaveRSA() {
    }

}
